At this point, you have seen many ways to obtain FileStream, StreamReader, and StreamWriter objects, but you have yet to read data from or write data to a file using these types. To understand how to do this, you’ll need to familiarize yourself with the concept of a stream. In the world of I/O manipulation, a stream represents a chunk of data flowing between a source and a destination. Streams provide a common way to interact with a sequence of bytes, regardless of what kind of device (e.g., file, network connection, and printer) stores or displays the bytes in question.
The abstract System.IO.Stream class defines several members that provide support for synchronous and asynchronous interactions with the storage medium (e.g., an underlying file or memory location).
Note The concept of a stream is not limited to file IO. To be sure, the .NET libraries provide stream access to networks, memory locations, and other stream-centric abstractions.
Again, Stream descendents represent data as a raw stream of bytes; therefore, working directly with raw streams can be quite cryptic. Some Stream-derived types support seeking, which refers to the process of obtaining and adjusting the current position in the stream. Table 20-7 helps you understand the functionality provided by the Stream class by describing its core members.
Table 20-7. Abstract Stream Members
Member | Meaning in Life |
---|---|
CanRead CanWrite CanSeek | Determines whether the current stream supports reading, seeking, and/or writing. |
Close() | Closes the current stream and releases any resources (such as sockets and file handles) associated with the current stream. Internally, this method is aliased to the Dispose() method; therefore closing a stream is functionally equivalent to disposing a stream. |
Flush() | Updates the underlying data source or repository with the current state of the buffer and then clears the buffer. If a stream does not implement a buffer, this method does nothing. |
Length | Returns the length of the stream in bytes. |
Position | Determines the position in the current stream. |
Read() ReadByte() | Reads a sequence of bytes (or a single byte) from the current stream and advances the current position in the stream by the number of bytes read. |
Seek() | Sets the position in the current stream. |
SetLength() | Sets the length of the current stream. |
Write() WriteByte() | Writes a sequence of bytes (or a single byte) to the current stream and advances the current position in this stream by the number of bytes written. |
The FileStream class provides an implementation for the abstract Stream members in a manner appropriate for file-based streaming. It is a fairly primitive stream; it can read or write only a single byte or an array of bytes. However, you will not often need to interact directly with the members of the FileStream type. Instead, you will probably use various stream wrappers, which make it easier to work with textual data or .NET types. Nevertheless, you will find it helpful to experiment with the synchronous read/write capabilities of the FileStream type.
Assume you have a new Console Application named FileStreamApp, and you imported System.IO and System.Text into your C# code file. Your goal is to write a simple text message to a new file named myMessage.dat. However, given that FileStream can operate only on raw bytes, you will be required to encode the System.String type into a corresponding byte array. Fortunately, the System.Text namespace defines a type named Encoding that provides members that encode and decode strings to (or from) an array of bytes (check out the .NET Framework 4.0 SDK documentation for more details about the Encoding type).
Once encoded, the byte array is persisted to file with the FileStream.Write() method. To read the bytes back into memory, you must reset the internal position of the stream (using the Position property) and call the ReadByte() method. Finally, you display the raw byte array and the decoded string to the console. Here is the complete Main() method:
// Don't forget to import the System.Text and System.IO namespaces. static void Main(string[] args) { Console.WriteLine("***** Fun with FileStreams *****\n"); // Obtain a FileStream object. using(FileStream fStream = File.Open(@"C:\myMessage.dat", FileMode.Create)) { // Encode a string as an array of bytes. string msg = "Hello!"; byte[] msgAsByteArray = Encoding.Default.GetBytes(msg); // Write byte[] to file. fStream.Write(msgAsByteArray, 0, msgAsByteArray.Length); // Reset internal position of stream. fStream.Position = 0; // Read the types from file and display to console. Console.Write("Your message as an array of bytes: "); byte[] bytesFromFile = new byte[msgAsByteArray.Length]; for (int i = 0; i < msgAsByteArray.Length; i++) { bytesFromFile[i] = (byte)fStream.ReadByte(); Console.Write(bytesFromFile[i]); } // Display decoded messages. Console.Write("\nDecoded Message: "); Console.WriteLine(Encoding.Default.GetString(bytesFromFile)); } Console.ReadLine(); }
This example populates the file with data, but it also punctuates the major downfall of working directly with the FileStream type: it demands to operate on raw bytes. Other Stream-derived types operate in a similar manner. For example, if you wish to write a sequence of bytes to a region of memory, you can allocate a MemoryStream. Likewise, if you wish to push an array of bytes through a network connection, you can use the NetworkStream class (in the System.Net.Sockets namespace).
As mentioned previously, the System.IO namespace provides several reader and writer types that encapsulate the details of working with Stream-derived types.
Source Code You can find the FileStreamApp project is under the Chapter 20 subdirectory.